package com.ruleengine.admin.controller;

import static com.ruleengine.worker.runner.rule.RuleArgsField.COMPUTE_RULE_FILTER_KEY;
import static com.ruleengine.worker.runner.rule.RuleArgsField.COMPUTE_RULE_MAP_KEY;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import com.ruleengine.coordinator.ICoordinator;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.ruleengine.admin.NotifyWorkerClient;
import com.ruleengine.admin.vo.CreateRulechainRequest;
import com.ruleengine.admin.vo.CreateRulechainResponse;
import com.ruleengine.admin.vo.DeleteRulechainRequest;
import com.ruleengine.admin.vo.Destination;
import com.ruleengine.admin.vo.GetRulechainResponse;
import com.ruleengine.admin.vo.UpdateRulechainRequest;
import com.ruleengine.admin.vo.UpdateRulechainStateRequest;
import com.ruleengine.db.model.Rule;
import com.ruleengine.db.model.RuleType;
import com.ruleengine.db.model.Rulechain;
import com.ruleengine.db.model.RulechainState;
import com.ruleengine.db.model.RulechainStatus;
import com.ruleengine.db.model.Ruleengine;
import com.ruleengine.db.model.Rulerelation;
import com.ruleengine.db.service.RuleService;
import com.ruleengine.db.service.RulechainService;
import com.ruleengine.db.service.RuleengineService;
import com.ruleengine.db.service.RulerelationService;
import com.ruleengine.util.exception.RuleEngineException;

import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import jakarta.validation.Valid;

/**
 * @author lianghanwei on 2024/3/15
 */
@RestController
@RequestMapping(value = "/v1/ruleengine/bifromq", produces = "application/json;charset=UTF-8")
@Slf4j
public class RuleChainController {

    private final RuleengineService ruleengineService;
    private final RulechainService rulechainService;
    private final RuleService ruleService;
    private final RulerelationService rulerelationService;
    private final NotifyWorkerClient notifyWorkerClient;
    private final ICoordinator coordinator;

    public RuleChainController(RuleengineService ruleengineService,
                               RulechainService rulechainService,
                               RuleService ruleService,
                               RulerelationService rulerelationService,
                               NotifyWorkerClient notifyWorkerClient,
                               ICoordinator coordinator) {
        this.ruleengineService = ruleengineService;
        this.rulechainService = rulechainService;
        this.ruleService = ruleService;
        this.rulerelationService = rulerelationService;
        this.notifyWorkerClient = notifyWorkerClient;
        this.coordinator = coordinator;
    }

    @PostMapping("/{instanceId}/rulechain")
    public CreateRulechainResponse createRuleChain(
        @RequestParam("tenantId") String tenantId,
        @PathVariable("instanceId") String instanceId,
        @Valid @RequestBody CreateRulechainRequest request) {

        Ruleengine ruleengine = ruleengineService.getRuleengine(tenantId, instanceId);

        Rulechain rulechain = Rulechain.builder()
            .tenantId(ruleengine.getTenantId())
            .ruleengineId(ruleengine.getRuleengineId())
            .rulechainId(IdUtil.simpleUUID())
            .name(request.getName())
            .description(request.getDescription())
            .resourceVersion(0)
            .state(RulechainState.DISABLED)
            .status(RulechainStatus.PENDING)
            .build();

        rulechainService.save(rulechain);

        CreateRulechainResponse response = new CreateRulechainResponse();
        response.setRulechainId(rulechain.getRulechainId());

        return response;
    }

    @DeleteMapping("/{instanceId}/rulechain")
    public void deleteRuleChain(
        @RequestParam("tenantId") String tenantId,
        @PathVariable("instanceId") String instanceId,
        @Valid @RequestBody DeleteRulechainRequest request) {
        List<String> ruleChainIds = request.getRulechainIds();
        List<Rulechain> ruleChains = rulechainService.getRulechains(tenantId, ruleChainIds);

        if (ruleChainIds.size() != ruleChains.size()) {
            throw new RuleEngineException("Some rulechains not found");
        }

        ruleChains.forEach(rulechain -> {
            if (RulechainState.ENABLED == rulechain.getState()) {
                throw new RuleEngineException("Can't delete ENABLED rulechains");
            }
        });

        rulechainService.softDeleteRulechains(tenantId, ruleChainIds);
    }

    @PutMapping("/{instanceId}/rulechain/{rulechainId}")
    public void updateRuleChain(
        @RequestParam("tenantId") String tenantId,
        @PathVariable("instanceId") String instanceId,
        @PathVariable("rulechainId") String ruleChainId,
        @RequestBody UpdateRulechainRequest request) {
        Rulechain rulechain = rulechainService.getRulechain(tenantId, ruleChainId).orElseThrow();

        List<Rule> computeRules = buildComputeRules(tenantId, ruleChainId,
                request.getFilterExpression(), request.getMapExpression());
        List<Rule> destRules =  buildDestinationRules(tenantId, ruleChainId, request.getDestinations());
        List<Rulerelation> relations = buildRuleRelations(computeRules, destRules);

        // TODO-lhw-2024/03/24: 事务
        rulechainService.updateRulechainDescriptionAndTopic(tenantId, ruleChainId,
                request.getDescription(), request.getTopic(), rulechain.getResourceVersion());

        ruleService.softDeleteRulesByRulechainId(tenantId, ruleChainId);
        ruleService.saveBatch(Stream.of(computeRules, destRules).flatMap(Collection::stream)
                .collect(Collectors.toList()));

        rulerelationService.softDeleteRulerelationsByRulechainId(tenantId, ruleChainId);
        rulerelationService.saveBatch(relations);

        if (RulechainState.ENABLED == rulechain.getState()) {
            notifyWorkerClient.notifyRulechainUpdated(ruleChainId);
        }
    }

    @PutMapping(value = "/{instanceId}/rulechain/{rulechainId}", params = {"updateState"})
    public void updateRulechainState(
        @RequestParam("tenantId") String tenantId,
        @PathVariable("instanceId") String instanceId,
        @PathVariable("rulechainId") String ruleChainId,
        @RequestBody UpdateRulechainStateRequest request) {
        Rulechain rulechain = rulechainService.getRulechain(tenantId, ruleChainId).orElseThrow();
        RulechainState originalState = rulechain.getState();
        RulechainState newState = RulechainState.valueOf(request.getState().name());

        if (RulechainState.ENABLED == newState && RulechainState.DISABLED == originalState) {
            rulechainService.updateRulechainState(tenantId, ruleChainId, newState, rulechain.getResourceVersion());
            coordinator.sub(rulechain.getTopic())
                    .whenComplete((v, e) -> {
                        if (e != null) {
                            log.error("failed to sub: ", e);
                        }
                    });
        } else if (RulechainState.DISABLED == newState && RulechainState.ENABLED == originalState) {
            rulechainService.updateRulechainState(tenantId, ruleChainId, newState, rulechain.getResourceVersion());
            coordinator.unsub(rulechain.getTopic())
                    .whenComplete((v, e) -> {
                       if (e != null) {
                           log.error("failed to unsub: ", e);
                       }
                    });
        }
    }

    @GetMapping("/{instanceId}/rulechain/{rulechainId}")
    public GetRulechainResponse getRulechain(
        @RequestParam("tenantId") String tenantId,
        @PathVariable("instanceId") String instanceId,
        @PathVariable("rulechainId") String ruleChainId) {
        Rulechain rulechain = rulechainService.getRulechain(tenantId, ruleChainId).orElseThrow();
        List<Rule> rules = ruleService.getRules(tenantId, ruleChainId);

        List<Destination> destinations = rules.stream()
            .filter(rule -> rule.getType().isDestRuleType())
            .map(rule -> buildDestination(rule))
            .collect(Collectors.toList());

        Optional<String> filterExpressionOpt = rules.stream()
            .filter(rule -> rule.getType() == RuleType.JSON_FILTER)
            .map(rule -> (String) JSONUtil.toBean(rule.getArgs(), Map.class).get(COMPUTE_RULE_FILTER_KEY))
            .findFirst();

        Optional<String> mapExpressionOpt = rules.stream()
            .filter(rule -> rule.getType() == RuleType.JSON_MAP)
            .map(rule -> (String) JSONUtil.toBean(rule.getArgs(), Map.class).get(COMPUTE_RULE_MAP_KEY))
            .findFirst();

        return GetRulechainResponse.builder()
            .name(rulechain.getName())
            .description(rulechain.getDescription())
            .topic(rulechain.getTopic())
            .filterExpression(filterExpressionOpt.orElse(StrUtil.EMPTY))
            .mapExpression(mapExpressionOpt.orElse(StrUtil.EMPTY))
            .destinations(destinations)
            .status(rulechain.getStatus().name())
            .createTime(rulechain.getCreateTime())
            .build();
    }

    private Destination buildDestination(Rule destRule) {
        Destination destination = new Destination();
        destination.setArgs(JSONUtil.toBean(destRule.getArgs(), Map.class));

        switch (destRule.getType()) {
            case DEST_BIFROMQ:
                destination.setType(Destination.DestinationType.BIFROMQ);
                break;
            case DEST_KAFKA:
                destination.setType(Destination.DestinationType.KAFKA);
                break;
            default:
                throw new RuleEngineException("Unsupported destRule type: " + destRule.getType());
        }

        return destination;
    }

    private List<Rule> buildComputeRules(String tenantId,
                                         String ruleChainId,
                                         String filterExpression,
                                         String mapExpression) {
        List<Rule> computeRules = new ArrayList<>(2);

        if (StrUtil.isNotBlank(filterExpression)) {
            computeRules.add(Rule.builder()
                .tenantId(tenantId)
                .rulechainId(ruleChainId)
                .ruleId(IdUtil.simpleUUID())
                .type(RuleType.JSON_FILTER)
                .args(JSONUtil.toJsonStr(Map.of(COMPUTE_RULE_FILTER_KEY, filterExpression)))
                .build());
        }

        if (StrUtil.isNotBlank(mapExpression)) {
            computeRules.add(Rule.builder()
                .tenantId(tenantId)
                .rulechainId(ruleChainId)
                .ruleId(IdUtil.simpleUUID())
                .type(RuleType.JSON_MAP)
                .args(JSONUtil.toJsonStr(Map.of(COMPUTE_RULE_MAP_KEY, mapExpression)))
                .build());
        }

        return computeRules;
    }

    private List<Rule> buildDestinationRules(String tenantId,
                                             String ruleChainId,
                                             List<Destination> destinations) {
        return destinations.stream()
            .map(dest -> buildDestRule(tenantId, ruleChainId, dest))
            .collect(Collectors.toList());
    }

    private Rule buildDestRule(String tenantId,
                               String ruleChainId,
                               Destination destination) {
        switch (destination.getType()) {
            case BIFROMQ:
                return Rule.builder()
                    .tenantId(tenantId)
                    .rulechainId(ruleChainId)
                    .ruleId(IdUtil.simpleUUID())
                    .type(RuleType.DEST_BIFROMQ)
                    .args(JSONUtil.toJsonStr(destination.getArgs()))
                    .build();
            case KAFKA:
                return Rule.builder()
                    .tenantId(tenantId)
                    .rulechainId(ruleChainId)
                    .ruleId(IdUtil.simpleUUID())
                    .type(RuleType.DEST_KAFKA)
                    .args(JSONUtil.toJsonStr(destination.getArgs()))
                    .build();
            default:
                throw new RuleEngineException("Unsupported destination type: " + destination.getType());
        }
    }

    private List<Rulerelation> buildRuleRelations(List<Rule> computeRules,
                                                  List<Rule> destRules) {
        switch (computeRules.size()) {
            case 0:
                // not need to build rulerelation
            case 1:
                Rule filterOrMapRule = computeRules.get(0);
                return buildRuleRelationsWithDests(filterOrMapRule.getRuleId(), destRules);
            case 2:
                Rule filterRule = computeRules.get(0);
                Rule mapRule = computeRules.get(1);
                List<Rulerelation> ruleRelations = new ArrayList<>();
                ruleRelations.add(buildRulerelation(filterRule.getTenantId(), filterRule.getRulechainId(),
                        filterRule.getRuleId(), mapRule.getRuleId()));
                ruleRelations.addAll(buildRuleRelationsWithDests(mapRule.getRuleId(), destRules));
                return ruleRelations;
            default:
                throw new RuleEngineException("Unsupported computeRules size: " + computeRules.size());
        }
    }

    private List<Rulerelation> buildRuleRelationsWithDests(String computeRuleId, List<Rule> destRules) {
        return destRules.stream()
            .map(destRule -> buildRulerelation(destRule.getTenantId(), destRule.getRulechainId(),
                    computeRuleId, destRule.getRuleId()))
            .collect(Collectors.toList());
    }

    private Rulerelation buildRulerelation(String tenantId,
                                           String ruleChainId,
                                           String upperRuleId,
                                           String downRuleId) {
        return Rulerelation.builder()
            .tenantId(tenantId)
            .rulechainId(ruleChainId)
            .rulerelationId(IdUtil.simpleUUID())
            .upperRuleId(upperRuleId)
            .downRuleId(downRuleId)
            .build();
    }
}